#Author: Salvatore Gionfriddo
from random import random, randint
from math import exp

class NeuralNetwork:
    def __init__(self, i_size, h_size, o_size):
        self.input_size = i_size
        self.hidden = []
        pi = i_size
        for i in h_size:
            self.hidden.append(NeuralNetworkLayer(i, pi))
            pi = i
        self.output = NeuralNetworkLayer(o_size, pi)
    def train(self, data, desired, max_MSE=0.001, learning_rate=0.1, num_runs=0):
        cur_runs = 0
        while True and cur_runs < num_runs:
            if num_runs != 0: cur_runs += 1
            output = self.run(data)
            MSE = 0
            output_weight_delta = []
            hidden_weight_delta = []
            # Determine new output weights
            for i in range(0, self.output.size):
                output_weight_delta.append( desired[i] - output[i] )
                MSE += output_weight_delta[i] * output_weight_delta[i]
                output_weight_delta[i] *= ( output[i] * ( 1 - output[i] ) )
            if MSE < max_MSE and num_runs == 0: break
            else: pass #print 'MSE',MSE
            # Determine new hidden weights
            weight_delta = output_weight_delta
            layer = self.output
            for k in range(0,len(self.hidden)):
                hidden_weight_delta.append([])
            for k in reversed(range(0, len(self.hidden))):
                for i in range(0, len(self.hidden[k])):
                    h_sum = 0
                    for j in range(0, self.output.size):
                        h_sum += weight_delta[j] * layer.nodes[j].weights[i]
                    h_out = self.hidden[k].nodes[i].output
                    hidden_weight_delta[k].append(h_sum * (h_out * ( 1 - h_out )))
                weight_delta = hidden_weight_delta[k]
                layer = self.hidden[k]
            # Apply weights
            self.output.applyWeightDelta(output_weight_delta, self.hidden[len(self.hidden)-1], learning_rate)
            for k in range(0, len(self.hidden)):
                if k == 0:
                    layer = data
                self.hidden[k].applyWeightDelta(hidden_weight_delta[k], layer, learning_rate)
    def run(self, data):
        for k in range(0,len(self.hidden)):
            self.hidden[k].run(data)
            data = self.hidden[k]
        self.output.run(self.hidden[len(self.hidden)-1])
        result = []
        for i in range(0, self.output.size):
            result.append(self.output[i])
        return result
    def crossproduct(self, a, b):
        c = []
        for i in range(0, len(a)):
            c.append(a[i]*b[i])
        return c

class NeuralNetworkLayer:
    def __init__(self, dim, num_weights):
        self.size = dim
        self.num_weights = num_weights
        self.nodes = []
        for j in range(0, dim):
            self.nodes.append(Neuron(num_weights))
    def __getitem__(self, n):
        return self.nodes[n].output
    def __len__(self):
        return len(self.nodes)
    def sigmoid(self, x):
        return 1 / ( 1 + exp(-x) )
    def run(self, data):
        for j in range(0, self.size):
            h_sum = 0
            for i in range(0, len(data)):
                h_sum += self.nodes[j].weights[i] * data[i]
            h_sum += self.nodes[j].weights[len(data)] #bias/threshold
            self.nodes[j].output = self.sigmoid(h_sum)
    def applyWeightDelta(self, weight_delta, data, learning_rate):
        #print 's',len(self.nodes[0].weights),'d',len(data),'m',self.size,len(weight_delta)
        for i in range(0, self.size):
            for j in range(0, len(data)):
                #print i,j
                self.nodes[i].weights[j] += learning_rate * weight_delta[i] * data[j]
            self.nodes[i].weights[len(data)] += learning_rate * weight_delta[i] #bias/threshold
    def determineWeightDelta(self):
        pass

class Neuron:
    def __init__(self, num_weights):
        self.weights = []
        self.output = 0
        for i in range(0, num_weights+1): #add one for the bias/threshhold
            self.weights.append(0.5 - random())

# Edit train to run X times instead of until MSE is < X
## Check
# Create a procedure to randomly generate training data
# Create a procedure to run training data until the error is low enough
# Only train if calculated value is not desired value + accuracy
# Memory system to store state values
def GenerateTrainingData():
    pass
def TrainData():
    pass
def Main():
    nnwork = NeuralNetwork(2,[3],2)
    data=[[0,0],[0,1],[1,0],[1,1]]
    outputs=[[0.5,0.5],[0.1,0.1],[0.9,0.9],[0.1,0.9]]
    trial_cases = 1000
    for i in range(0,trial_cases):
        for j in range(0,len(data)):
            nnwork.train(data[j], outputs[j], 0.001, 0.1)
    for i in range(0,len(data)):
        for j in range(0,len(outputs[0])):
            print nnwork.run(data[i])[j]
        print '---'
    nnwork.train([0,0], [0.1,0.9], 0.5, 0.1)
    print 'post'
    for i in range(0,len(data)):
        for j in range(0,len(outputs[i])):
            print nnwork.run(data[i])[j]
        print '---'

Main()
